<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';

import Graph from 'graphology';
import FA2 from 'graphology-layout-forceatlas2';
import { debounce } from 'lodash-es';
import Sigma from 'sigma';

interface NodeData {
  data: {
    entity_type?: string;
    icon?: string;
    id: string;
    label: string;
  };
}

interface EdgeData {
  data: {
    source: string;
    target: string;
  };
}

interface Elements {
  nodes: NodeData[];
  edges: EdgeData[];
}

const props = defineProps<{ elements: Elements }>();
const emit = defineEmits(['nodeClick']);

const container = ref<HTMLDivElement | null>(null);
const searchTerm = ref('');
const isLocked = ref(false);
const showLabels = ref(true);
const visibleNodes = ref(new Set<string>());

let renderer: null | Sigma = null;
let graph: Graph | null = null;
const iconCache = new Map<string, HTMLImageElement>();

function getRandomColor() {
  const palette = ['#60A5FA', '#34D399', '#FBBF24', '#F87171', '#A78BFA'];
  return palette[Math.floor(Math.random() * palette.length)];
}

function buildGraph(elements: Elements) {
  const g = new Graph({ type: 'undirected' });

  // Add nodes with compact initial distribution
  elements.nodes.forEach(({ data }) => {
    const { id, label, icon, entity_type } = data;
    g.addNode(id, {
      label,
      labelOriginal: label.toLowerCase(),
      icon,
      entity_type,
      x: (Math.random() - 0.5) * 500,
      y: (Math.random() - 0.5) * 500,
      size: 8,
      color: getRandomColor(),
      highlighted: true,
      hidden: false,
      showLabel: false,
    });
  });

  // Add edges
  elements.edges.forEach(({ data }) => {
    const { source, target } = data;
    g.addEdge(source, target, {
      size: 0.5,
      color: 'rgba(153,153,153,0.5)',
      highlighted: true,
    });
  });

  // Optimized force layout
  FA2.assign(g, {
    iterations: 300,
    settings: {
      gravity: 0.05,
      scalingRatio: 50,
      slowDown: 10,
      barnesHutOptimize: true,
      edgeWeightInfluence: 0.2,
      outboundAttractionDistribution: true,
      linLogMode: true,
    },
  });

  return g;
}

function renderGraph(g: Graph) {
  if (!container.value) return;
  if (renderer) renderer.kill();

  renderer = new Sigma(g, container.value, {
    renderLabels: (node, ctx, settings) => {
      if (!showLabels.value) return;

      const zoom = settings.zoomRatio ?? 1;
      const label = g.getNodeAttribute(node, 'label');
      const size = g.getNodeAttribute(node, 'size');
      const x = g.getNodeAttribute(node, 'x');
      const y = g.getNodeAttribute(node, 'y');
      const icon = g.getNodeAttribute(node, 'icon');
      const highlighted = g.getNodeAttribute(node, 'highlighted');
      const showLabel = g.getNodeAttribute(node, 'showLabel');

      // Draw icon if exists
      if (icon) {
        const cached = iconCache.get(icon);
        if (cached) {
          ctx.drawImage(cached, x - size, y - size, size * 2, size * 2);
        } else {
          const image = new Image();
          image.src = icon;
          image.addEventListener('load', () => {
            iconCache.set(icon, image);
            ctx.drawImage(image, x - size, y - size, size * 2, size * 2);
          });
        }
      }

      // Only show label when appropriate
      if (!showLabel && !highlighted) return;

      ctx.fillStyle = highlighted ? '#374151' : 'rgba(55, 65, 81, 0.7)';
      const fontSize = Math.min(14, Math.max(10, 12 * Math.sqrt(zoom)));
      ctx.font = `${Math.round(fontSize)}px Inter, sans-serif`;
      ctx.fillText(label, x + size * 1.2, y);
    },
    nodeReducer: (node, data) => {
      const matchesSearch =
        !searchTerm.value ||
        data.labelOriginal?.includes(searchTerm.value.toLowerCase());
      const isVisible = !data.hidden && (matchesSearch || data.highlighted);

      return {
        ...data,
        label: isVisible ? data.labelOriginal : '',
        color: data.highlighted ? data.color : 'rgba(229, 231, 235, 0.5)',
        size: isVisible
          ? Math.min(
              20,
              Math.max(
                6,
                data.size * (renderer?.getCamera().getState().ratio || 1) * 0.3,
              ),
            )
          : 0,
        hidden: !isVisible,
      };
    },
    edgeReducer: (edge, data) => {
      const source = g.source(edge);
      const target = g.target(edge);
      const sourceData = g.getNodeAttributes(source);
      const targetData = g.getNodeAttributes(target);

      if (sourceData.hidden || targetData.hidden) {
        return { ...data, hidden: true };
      }

      const isHighlighted = sourceData.highlighted && targetData.highlighted;
      return {
        ...data,
        color: isHighlighted
          ? 'rgba(153,153,153,0.8)'
          : 'rgba(200,200,200,0.2)',
        size: isHighlighted ? 1 : 0.3,
        hidden: false,
      };
    },
    labelDensity: 0.07,
    labelGridCellSize: 100,
    labelRenderedSizeThreshold: 0.5,
  });

  // Optimized camera update with debounce
  const debouncedRefresh = debounce(() => {
    const cameraState = renderer?.getCamera().getState();
    if (!cameraState || !graph) return;

    const newVisibleNodes = new Set<string>();
    const { ratio: zoom, x, y } = cameraState;

    graph.forEachNode((node, attr) => {
      const isVisible = renderer?.getCamera().isNodeVisible(node);
      newVisibleNodes.add(node);
      graph?.setNodeAttribute(node, 'hidden', !isVisible);

      if (isVisible) {
        // Calculate label visibility based on distance and zoom
        const distance = Math.hypot(attr.x - x, attr.y - y);
        const labelScale = zoom * (1000 / Math.max(1, distance));
        graph.setNodeAttribute(node, 'showLabel', labelScale > 0.7);

        // Adjust size based on zoom
        const zoomAdjustedSize = Math.min(20, Math.max(6, 8 * zoom * 0.4));
        graph.setNodeAttribute(node, 'size', zoomAdjustedSize);
      }
    });

    visibleNodes.value = newVisibleNodes;
    renderer?.refresh();
  }, 50);

  renderer.getCamera().on('updated', debouncedRefresh);

  // Interaction handlers
  renderer.on('clickNode', ({ node }) => {
    emit('nodeClick', g.getNodeAttributes(node));
  });

  renderer.on('enterNode', ({ node }) => {
    const neighbors = new Set(g.neighbors(node));
    neighbors.add(node);

    g.forEachNode((n) => {
      g.setNodeAttribute(n, 'highlighted', neighbors.has(n));
    });

    g.forEachEdge((edge) => {
      const [source, target] = g.extremities(edge);
      g.setEdgeAttribute(
        edge,
        'highlighted',
        neighbors.has(source) && neighbors.has(target),
      );
    });

    debouncedRefresh();
  });

  renderer.on('leaveNode', () => {
    g.forEachNode((n) => g.setNodeAttribute(n, 'highlighted', true));
    g.forEachEdge((e) => g.setEdgeAttribute(e, 'highlighted', true));
    debouncedRefresh();
  });

  // Initial zoom adjustment
  setTimeout(() => {
    renderer?.getCamera().animatedZoom({ duration: 500 });
  }, 100);
}

function resetZoom() {
  renderer?.getCamera().animatedReset({ duration: 500 });
}

function toggleLock() {
  if (!graph) return;
  isLocked.value = !isLocked.value;
  graph.forEachNode((node) => {
    graph!.setNodeAttribute(node, 'draggable', !isLocked.value);
  });
}

function toggleLabels() {
  showLabels.value = !showLabels.value;
  renderer?.refresh();
}

function clusterByType() {
  if (!graph || !renderer) return;

  const clusters = new Map<string, string[]>();
  graph.forEachNode((node, attr) => {
    const type = attr.entity_type || 'default';
    if (!clusters.has(type)) clusters.set(type, []);
    clusters.get(type)?.push(node);
  });

  let angle = 0;
  const step = (2 * Math.PI) / clusters.size;
  const radius = Math.min(500, 100 * Math.sqrt(clusters.size));

  clusters.forEach((nodes) => {
    const centerX = radius * Math.cos(angle);
    const centerY = radius * Math.sin(angle);

    nodes.forEach((node) => {
      graph?.setNodeAttribute(
        node,
        'x',
        centerX + (Math.random() - 0.5) * radius * 0.3,
      );
      graph?.setNodeAttribute(
        node,
        'y',
        centerY + (Math.random() - 0.5) * radius * 0.3,
      );
    });

    angle += step;
  });

  renderer.refresh();
}

onMounted(() => {
  graph = buildGraph(props.elements);
  renderGraph(graph);
});

watch(
  () => props.elements,
  (newElements) => {
    graph = buildGraph(newElements);
    renderGraph(graph);
  },
  { deep: true },
);

watch(searchTerm, () => {
  if (!graph || !renderer) return;
  renderer.refresh();
});
</script>

<template>
  <div class="relative h-full w-full">
    <div
      class="absolute left-2 top-2 z-10 flex flex-col gap-2 rounded-lg bg-white p-2 shadow"
    >
      <input
        v-model="searchTerm"
        type="text"
        placeholder="搜索节点"
        class="rounded border border-gray-300 px-2 py-1 text-sm"
      />
      <div class="flex gap-2">
        <button @click="resetZoom" class="text-sm text-blue-500">
          重置缩放
        </button>
        <button @click="toggleLock" class="text-sm text-blue-500">
          {{ isLocked ? '解锁拖动' : '锁定拖动' }}
        </button>
        <button @click="toggleLabels" class="text-sm text-blue-500">
          {{ showLabels ? '隐藏标签' : '显示标签' }}
        </button>
        <button @click="clusterByType" class="text-sm text-blue-500">
          按类型分组
        </button>
      </div>
    </div>
    <div ref="container" class="h-full w-full"></div>
  </div>
</template>

<style scoped>
input:focus {
  outline: none;
  box-shadow: 0 0 0 2px rgb(59 130 246 / 30%);
}

:deep(canvas) {
  cursor: grab;
}
</style>
